﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml;
using Common.Logging;
using Spring.Util;

namespace Spring.Context.Support
{
    /// <summary>
    /// Creates an <see cref="Spring.Context.IApplicationContext"/> instance
    /// using context definitions supplied in a custom configuration and
    /// configures the <see cref="ContextRegistry"/> with that instance.
    /// </summary>
    /// <remarks>
    /// Implementations of the <see cref="Spring.Context.IApplicationContext"/>
    /// interface <b>must</b> provide the following two constructors:
    /// <list type="number">
    /// <item>
    /// <description>
    /// A constructor that takes a string array of resource locations.
    /// </description>
    /// </item>
    /// <item>
    /// <description>
    /// A constructor that takes a reference to a parent application context
    /// and a string array of resource locations (and in that order).
    /// </description>
    /// </item>
    /// </list>
    /// <p>
    /// Note that if the <c>type</c> attribute is not present in the declaration
    /// of a particular context, then a default
    /// <see cref="Spring.Context.IApplicationContext"/> <see cref="System.Type"/>
    /// is assumed. This default
    /// <see cref="Spring.Context.IApplicationContext"/> <see cref="System.Type"/>
    /// is currently the <see cref="Spring.Context.Support.XmlApplicationContext"/>
    /// <see cref="System.Type"/>; please note the exact <see cref="System.Type"/>
    /// of this default <see cref="Spring.Context.IApplicationContext"/> is an
    /// implementation detail, that, while unlikely, may do so in the future.
    /// to
    /// </p>
    /// </remarks>
    /// <example>
    /// <p>
    /// This is an example of specifying a context that reads its resources from
    /// an embedded Spring.NET XML object configuration file...
    /// </p>
    /// <code escaped="true">
    /// <configuration>
    ///     <configSections>
    ///		    <sectionGroup name="spring">
    ///			    <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core"/>
    ///		    </sectionGroup>
    ///     </configSections>
    ///     <spring>
    ///		    <context>
    ///			    <resource uri="assembly://MyAssemblyName/MyResourceNamespace/MyObjects.xml"/>
    ///             <resource uri="directory://configuration" file-pattern="*.xml"/>
    ///             <resource uri="directory://configuration" file-pattern="*.abc.dll" resource-pattern="{0}/{1}.Remoting.xml">
    ///                 <separator>.</separator>
    ///             </resource>
    ///		    </context>
    ///     </spring>
    /// </configuration>
    /// </code>
    /// <p>
    /// This is an example of specifying a context that reads its resources from
    /// a custom configuration section within the same application / web
    /// configuration file and uses case insensitive object lookups. 
    /// </p>
    /// <p>
    /// Please note that you <b>must</b> adhere to the naming
    /// of the various sections (i.e. '&lt;sectionGroup name="spring"&gt;' and
    /// '&lt;section name="context"&gt;'.
    /// </p>
    /// <code escaped="true">
    /// <configuration>
    ///     <configSections>
    ///		    <sectionGroup name="spring">
    ///			    <section name="context"
    ///				    type="Spring.Context.Support.ContextHandler, Spring.Core"/>
    ///			    <section name="objects"
    ///				    type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
    ///		    </sectionGroup>
    ///     </configSections>
    ///     <spring>
    ///		    <context
    ///		        caseSensitive="false"
    ///			    type="Spring.Context.Support.XmlApplicationContext, Spring.Core">
    ///			    <resource uri="config://spring/objects"/>
    ///			    <resource uri="directory://configuration" file-pattern="*.xml" />
    ///		    </context>
    ///		    <objects xmlns="http://www.springframework.net">
    ///			    <!-- object definitions... -->
    ///		    </objects>
    ///     </spring>
    /// </configuration>
    /// </code>
    /// <p>
    /// And this is an example of specifying a hierarchy of contexts. The
    /// hierarchy in this case is only a simple parent->child hierarchy, but
    /// hopefully it illustrates the nesting of context configurations. This
    /// nesting of contexts can be arbitrarily deep, and is one way... child
    /// contexts know about their parent contexts, but parent contexts do not
    /// know how many child contexts they have (if any), or have references
    /// to any such child contexts.
    /// </p>
    /// <code escaped="true">
    /// <configuration>
    /// 	<configSections>
    /// 		<sectionGroup name='spring'>
    /// 			<section name='context'	type='Spring.Context.Support.ContextHandler, Spring.Core'/>
    /// 			<section name='objects'	type='Spring.Context.Support.DefaultSectionHandler, Spring.Core' />
    ///             <sectionGroup name="child">
    /// 			    <section name='objects' type='Spring.Context.Support.DefaultSectionHandler, Spring.Core' />
    ///             </sectionGroup>
    /// 		</sectionGroup>
    /// 	</configSections>
    /// 	
    ///     <spring>
    /// 		<context name='Parent'>
    /// 			<resource uri='config://spring/objects'/>
    /// 		    <context name='Child'>
    /// 			    <resource uri='config://spring/childObjects'/>
    /// 		    </context>
    /// 		</context>
    /// 		<!-- parent context's objects -->
    /// 		<objects xmlns='http://www.springframework.net'>
    /// 			<object id='Parent' type='Spring.Objects.TestObject,Spring.Core.Tests'>
    ///                 <property name='name' value='Parent'/>
    ///             </object>
    /// 		</objects>
    /// 		<!-- child context's objects -->
    ///         <child>
    /// 		    <objects xmlns='http://www.springframework.net'>
    /// 			    <object id='Child' type='Spring.Objects.TestObject,Spring.Core.Tests'>
    ///                     <property name='name' value='Child'/>
    ///                 </object>
    /// 		    </objects>
    ///         </child>
    ///     </spring>
    /// </configuration>
    /// </code>
    /// </example>
    /// <author>Mark Pollack</author>
    /// <author>Aleksandar Seovic</author>
    /// <author>Rick Evans</author>
    /// <seealso cref="ContextRegistry"/>
    public class DynamicContextHandler : ContextHandler, IConfigurationSectionHandler
    {
        private static readonly ILog log = LogManager.GetLogger(typeof(ContextHandler));

        /// <summary>
        /// Creates an <see cref="Spring.Context.IApplicationContext"/> instance
        /// using the context definitions supplied in a custom
        /// configuration section.
        /// </summary>
        /// <remarks>
        /// <p>
        /// This <see cref="Spring.Context.IApplicationContext"/> instance is
        /// also used to configure the <see cref="ContextRegistry"/>.
        /// </p>
        /// </remarks>
        /// <param name="parent">
        /// The configuration settings in a corresponding parent
        /// configuration section.
        /// </param>
        /// <param name="configContext">
        /// The configuration context when called from the ASP.NET
        /// configuration system. Otherwise, this parameter is reserved and
        /// is <see langword="null"/>.
        /// </param>
        /// <param name="section">
        /// The <see cref="System.Xml.XmlNode"/> for the section.
        /// </param>
        /// <returns>
        /// An <see cref="Spring.Context.IApplicationContext"/> instance
        /// populated with the object definitions supplied in the configuration
        /// section.
        /// </returns>
        public new object Create(object parent, object configContext, XmlNode section)
        {
            var contextElement = section as XmlElement;
            if (contextElement == null)
            {
                throw ConfigurationUtils.CreateConfigurationException("Context configuration section must be an XmlElement.");
            }
            if ((parent != null) && !(parent is IApplicationContext))
            {
                throw ConfigurationUtils.CreateConfigurationException(string.Format("Parent context must be of type IApplicationContext, but was '{0}'", parent.GetType().FullName));
            }
            var contextName = GetContextName(configContext, contextElement);
            if (!StringUtils.HasLength(contextName))
            {
                contextName = "spring.root";
            }
            if (log.IsDebugEnabled)
            {
                log.Debug(string.Format("creating context '{0}'", contextName));
            }
            IApplicationContext context;
            try
            {
                var parentContext = parent as IApplicationContext;

                var contextType = (Type)typeof(ContextHandler).GetMethod("GetContextType", BindingFlags.NonPublic | BindingFlags.Instance).
                    Invoke(this, new object[] { contextElement, parentContext });

                var caseSensitivity = (bool)typeof(ContextHandler).GetMethod("GetCaseSensitivity", BindingFlags.NonPublic | BindingFlags.Instance).
                    Invoke(this, new object[] { contextElement });

                string[] resources = GetResources(contextElement);
                context = InstantiateContext(parentContext, configContext, contextName, contextType, caseSensitivity, resources);
                if (AutoRegisterWithContextRegistry && !ContextRegistry.IsContextRegistered(context.Name))
                    ContextRegistry.RegisterContext(context);
                var childContexts
                     = (XmlNode[])typeof(ContextHandler).GetMethod("GetChildContexts", BindingFlags.NonPublic | BindingFlags.Instance).
                    Invoke(this, new object[] { contextElement });
                CreateChildContexts(context, configContext, childContexts);
                if (log.IsDebugEnabled)
                    log.Debug(string.Format("context '{0}' created for name '{1}'", context, contextName));
            }
            catch (Exception exception)
            {
                if (!ConfigurationUtils.IsConfigurationException(exception))
                {
                    throw ConfigurationUtils.CreateConfigurationException(string.Format("Error creating context '{0}': {1}", contextName, ReflectionUtils.GetExplicitBaseException(exception).Message), exception);
                }
                throw;
            }
            return context;

        }

        /// <summary abc="dd">
        /// Returns the array of resources containing object definitions for
        /// this context.
        /// </summary>
        private string[] GetResources(XmlElement contextElement)
        {
            var resourceNodes = new ArrayList(contextElement.ChildNodes.Count);
            foreach (XmlNode possibleResourceNode in contextElement.ChildNodes)
            {
                var possibleResourceElement = possibleResourceNode as XmlElement;
                if (possibleResourceElement != null &&
                   possibleResourceElement.LocalName == "resource")
                {
                    string resourceName = possibleResourceElement.GetAttribute("uri");
                    if (StringUtils.HasText(resourceName))
                    {
                        var uri = new Uri(resourceName);
                        if (uri.Scheme != "directory")
                            resourceNodes.Add(resourceName);
                        else
                        {
                            var directory = uri.Host + uri.PathAndQuery;
                            var subLevel = possibleResourceElement.GetAttribute("sub-level");
                            var fetchSubLevel = 1;
                            if (StringUtils.HasText(subLevel))
                                fetchSubLevel += int.Parse(subLevel);
                            if (StringUtils.HasText(directory))
                            {
                                var searchPattern = possibleResourceElement.GetAttribute("file-pattern");
                                var directorySearchPattern =
                                    possibleResourceElement.GetAttribute("directory-pattern");
                                var assemblyPattern = possibleResourceElement.GetAttribute("resource-pattern");

                                var listDir = new List<string>();
                                var queueDir = new Queue<string>();
                                queueDir.Enqueue(directory);
                                do
                                {
                                    var newQueue = new Queue<string>();
                                    while (queueDir.Count != 0)
                                    {
                                        var item = queueDir.Dequeue();
                                        listDir.Add(item);

                                        foreach (var sub in (StringUtils.HasText(directorySearchPattern)
                                                                       ? Directory.GetDirectories(item, directorySearchPattern)
                                                                       : Directory.GetDirectories(item)))
                                            queueDir.Enqueue(sub);
                                    }
                                    queueDir = newQueue;
                                    --fetchSubLevel;
                                } while (fetchSubLevel != 0 && queueDir.Count != 0);

                                foreach (var subdir in listDir)
                                {
                                    var listFile = StringUtils.HasText(searchPattern)
                                                       ? Directory.GetFiles(subdir, searchPattern)
                                                       : Directory.GetFiles(subdir);

                                    // Checking for corrected extensions and skip excepted file
                                    foreach (var file in listFile)
                                    {
                                        if (StringUtils.HasText(assemblyPattern) == false)
                                        {
                                            var resourceNameUrl = new Uri(new FileInfo(file).FullName);
                                            resourceNodes.Add(resourceNameUrl.AbsoluteUri);
                                        }
                                        else
                                        {
                                            var assemblyName = file.Replace((new FileInfo(file)).Extension, "");
                                            string[] array;
                                            if (!possibleResourceElement.HasChildNodes)
                                                array = assemblyName.Split('.');
                                            else
                                            {
                                                array =
                                                    assemblyName.Split((from object splitNode in possibleResourceElement
                                                                        select splitNode as XmlElement
                                                                            into splitElement
                                                                            where
                                                                                splitElement != null &&
                                                                                splitElement.LocalName == "separator"
                                                                            select splitElement.InnerText).ToArray(),
                                                                       StringSplitOptions.RemoveEmptyEntries);
                                            }

                                            var newArray = new string[array.Length + 1];
                                            newArray[0] = assemblyName;
                                            Array.Copy(array, 0, newArray, 1, array.Length);

                                            var pattern = String.Format(assemblyPattern, newArray);
                                            var resourceNameUrl = String.Format("assembly://{0}", pattern);
                                            resourceNodes.Add(resourceNameUrl);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            return (string[])resourceNodes.ToArray(typeof(string));
        }
    }
}
